﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Net;
using System.Diagnostics;

namespace MCWorldLibrary
{
    class Region
    {
        byte[] locations;
        byte[] timestamps;
        public Dictionary<Vector2, Chunk> chunks;

        public int X { get; private set; }
        public int Z { get; private set; }

        public Region(Vector2 coordinate)
        {
            this.X = coordinate.X;
            this.Z = coordinate.Y;
            this.locations = new byte[4096];
            this.timestamps = new byte[4096];
            this.chunks = new Dictionary<Vector2, Chunk>(new Vector2.EqualityComparer());
        }

        public void Load(string folder, int regionX, int regionZ)
        {
            this.X = regionX;
            this.Z = regionZ;

            List<int> pss = new List<int>();
            this.chunks.Clear();
            using (FileStream fs = new FileStream(Path.Combine(folder, string.Format("r.{0}.{1}.mca", regionX, regionZ)), FileMode.Open))
            {
                using (BinaryReader reader = new BinaryReader(fs))
                {
                    locations = reader.ReadBytes(4096);
                    timestamps = reader.ReadBytes(4096);
                    for (int x = regionX * 32; x < (regionX + 1) * 32; x++)
                    {
                        for (int z = regionZ * 32; z < (regionZ + 1) * 32; z++)
                        {

                            int i = GetChunkLocationOffset(x, z);

                            int pos = (IPAddress.NetworkToHostOrder(BitConverter.ToInt32(locations, i))) >> 8;

                            byte sectorCount = locations[i + 3];
                            
                            if (pos == 0 && sectorCount == 0)
                            {
                                //Console.WriteLine("Chunk ({0},{1}) does not exist", x, z);
                            }
                            else
                            {
                                Debug.WriteIf(sectorCount > 1, "Sector is large!");
                                pss.Add(pos);
  
                                int timestamp = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(timestamps, i));

                                reader.BaseStream.Position = pos * 4096;

                                int chunkLength = IPAddress.NetworkToHostOrder(reader.ReadInt32());
                                byte compression = reader.ReadByte();


                                byte[] chunkData = reader.ReadBytes(chunkLength - 1);

                                using (System.IO.MemoryStream mem = new MemoryStream(chunkData, 0, chunkLength - 1))
                                {
                                    using (MemoryStream decompressed = new MemoryStream())
                                    {
                                        using (zlib.ZOutputStream zout = new zlib.ZOutputStream(decompressed))
                                        {
                                            try
                                            {
                                                CopyStream(mem, zout);
                                                zout.finish();

                                                decompressed.Position = 0;

                                                NBT.Tag chunkDataTag = NBT.Tag.Deserialize(decompressed);
                                                
                                                int xpos = (((chunkDataTag.Payload as NBT.CompoundTagPayload).Value.Single().Payload as NBT.CompoundTagPayload).Value.First(o => o.Name == "xPos").Payload as NBT.IntPayload).Value;
                                                int zpos = (((chunkDataTag.Payload as NBT.CompoundTagPayload).Value.Single().Payload as NBT.CompoundTagPayload).Value.First(o => o.Name == "zPos").Payload as NBT.IntPayload).Value;

                                                this.chunks.Add(new Vector2(xpos, zpos), new Chunk(chunkDataTag));
                                                
                                                Debug.Assert(xpos == x);
                                                Debug.Assert(zpos == z);
                                                //Console.WriteLine("{0}, {1}", xpos, zpos);
                                            }
                                            catch (Exception ex)
                                            {
                                                // TA BORT DENNA TRY CATCH SEN
                                                Console.WriteLine("Fel på Chunk: ({0}, {1})", x, z);
                                                Console.WriteLine(ex.ToString());
                                            }

                                        }
                                    }
                                }
                            }

                        }
                    }

                }
            }
            Console.WriteLine("Percent loaded: {0:P}", this.chunks.Count() / (double)((32 * 32) - 1));
            //Console.WriteLine("----");
            //foreach (int vcc in pss.OrderBy(x => x))
            //    Console.WriteLine(vcc);
        }

        private void CopyStream(System.IO.Stream input, System.IO.Stream output)
        {
            byte[] buffer = new byte[2000];
            int len;
            while ((len = input.Read(buffer, 0, 2000)) > 0)
            {
                output.Write(buffer, 0, len);
            }
            output.Flush();
        }

        public void Save(string folder)
        {
            Array.Clear(locations, 0, 4096);
            using (FileStream fs = new FileStream(Path.Combine(folder, string.Format("r.{0}.{1}.mca",  X, Z)), FileMode.Create))
            {
                using (BigEndianBinaryWriter binaryWriter = new BigEndianBinaryWriter(fs))
                {
                    int chunkDataBytes = 2;
                    List<byte[]> chunkBlob = new List<Byte[]>();

                    foreach(Chunk chunk in chunks.Values)
                    {
                        using (MemoryStream compressed = new MemoryStream())
                        {
                            using (zlib.ZOutputStream zin = new zlib.ZOutputStream(compressed, zlib.zlibConst.Z_DEFAULT_COMPRESSION))
                            {
                                using (MemoryStream c = new MemoryStream())
                                {
                                    chunk.Serialize(c);
                                    c.Position = 0;
                                    CopyStream(c, zin);
                                    zin.finish();
                                }
                            }
                            byte[] dataBlob = compressed.ToArray();

                            List<byte> blob = new List<byte>();
                            blob.AddRange(BitConverter.GetBytes(IPAddress.NetworkToHostOrder(dataBlob.Length + 1)));
                            blob.Add(2);
                            blob.AddRange(dataBlob);

                            int remainder;
                            int noSectors = Math.DivRem(blob.Count, 4096, out remainder);
                            if (remainder > 0)
                            {
                                byte[] padding = new byte[4096 - remainder];
                                blob = blob.Concat(padding).ToList();
                                noSectors++;
                            }
                            //else
                            //{
                            //    throw new Exception("Nej vad i helvete!");
                            //}
                            chunkBlob.Add(blob.ToArray());



                            int x = chunk.X;
                            int z = chunk.Z;
                            int dataOffset = GetChunkLocationOffset(x, z);

                            byte[] offset = BitConverter.GetBytes(IPAddress.NetworkToHostOrder(chunkDataBytes << 8));
                            Buffer.BlockCopy(offset, 0, locations, dataOffset, 3);
                            locations[dataOffset + 3] = 1; // No idea

                            long unix = DateTime.UtcNow.ToUnixTime();
                            byte[] timestamp = BitConverter.GetBytes(IPAddress.NetworkToHostOrder((int)unix));
                            Buffer.BlockCopy(timestamp, 0, timestamps, dataOffset, 4);
                            

                            chunkDataBytes += noSectors;
                        }
                    }

                    binaryWriter.Write(locations);
                    binaryWriter.Write(timestamps);
                    for(int i = 0; i < chunkBlob.Count; ++i)
                    {
                        binaryWriter.Write(chunkBlob[i]);
                    }

                    binaryWriter.Flush();
                }
            }
        }

        private int GetChunkLocationOffset(int x, int z)
        {
            //  TODO: Handle negative x and z.
            x = x & 31;
            z = z & 31;
            //if (x < 0)
            //    x = 31 - Math.Abs(x % 32);
            //else
            //    x = x % 32;

            //if (z < 0)
            //    z = 31 - Math.Abs(z % 32);
            //else
            //    z = z % 32;
            

            return 4 * (x + z * 32);
        }

        public int GetXPositionFromRootTag(NBT.Tag tag)
        {
            return (((tag.Payload as NBT.CompoundTagPayload).Value.Single().Payload as NBT.CompoundTagPayload).Value.First(o => o.Name == "xPos").Payload as NBT.IntPayload).Value;
        }

        public int GetZPositionFromRootTag(NBT.Tag tag)
        {
            return (((tag.Payload as NBT.CompoundTagPayload).Value.Single().Payload as NBT.CompoundTagPayload).Value.First(o => o.Name == "zPos").Payload as NBT.IntPayload).Value;
        }

        public static bool Exists(string folder, int regionX, int regionZ)
        {
            return File.Exists(Path.Combine(folder, string.Format("r.{0}.{1}.mca", regionX, regionZ)));
        }

        public void SetBlocks(int chunkX, int chunkZ, int x0, int y0, int z0, int x1, int y1, int z1, byte[,,] blocks, BiomeType biomeType)
        {
            int chunkBlockLocationX = (chunkX * 16);
            int chunkBlockLocationZ = (chunkZ * 16);

            x0 -= chunkBlockLocationX;
            x1 -= chunkBlockLocationX;
            z0 -= chunkBlockLocationZ;
            z1 -= chunkBlockLocationZ;

            Chunk chunk = null;
            Vector2 coordinate = new Vector2(chunkX, chunkZ);
            if (chunks.ContainsKey(coordinate))
            {
                chunk = chunks[coordinate];
            }
            else
            {
                chunk = CreateNewChunk(chunkX, chunkZ, biomeType);
            }

            for (int y = y0; y < y1; y++)
            {
                int sectionIndex = (int)Math.Floor(y / 16.0);
                for (int x = x0; x < x1; x++)
                {
                    for (int z = z0; z < z1; z++)
                    {
                        chunk.Sections[sectionIndex].SetBlock(x & 15, y & 15, z & 15, blocks[x + chunkBlockLocationX, y, z + chunkBlockLocationZ]);
                    }
                }
            }
        }

        public void SetBlock(int x, int y, int z, byte block, BiomeType biomeType)
        {
            int chunkX = (int)Math.Floor(x / 16.0);
            int chunkZ = (int)Math.Floor(z / 16.0);

            x -= (chunkX * 16);
            z -= (chunkZ * 16);

            Chunk chunk = null;
            Vector2 coordinate = new Vector2(chunkX, chunkZ);
            if (chunks.ContainsKey(coordinate))
            {
                chunk = chunks[coordinate];
            }
            else
            {
                chunk = CreateNewChunk(chunkX, chunkZ, biomeType);
            }

            int chunkIndex = (int)Math.Floor(y / 16.0);
            y -= chunkIndex * 16;
            ChunkSection chunkSection = chunk.Sections[chunkIndex];

            chunkSection.SetBlock(x, y, z, block);
        }

        public void SetChunkBlocks(BlockData[, ,] blocks, Vector2 chunkCoordinate, Vector3 worldCoordinateStart, Vector3 worldCoordinateEnd, Vector3 blocksOffset, bool airOverwrites)
        {
            Chunk chunk = null;
            if (!chunks.TryGetValue(chunkCoordinate, out chunk))
            {
                chunk = CreateNewChunk(chunkCoordinate.X, chunkCoordinate.Y, BiomeType.Plains); // BiomeType = Uncalculated ?
            }

            Vector3 chunkWorldCoordinate = new Vector3(chunkCoordinate.X * 16, 0, chunkCoordinate.Y * 16) + blocksOffset;

            int sectionIndexStart = worldCoordinateStart.Y / 16;
            int sectionIndexEnd = (worldCoordinateEnd.Y-1) / 16;

            Vector3 sectionBlockStart = new Vector3(worldCoordinateStart);
            Vector3 sectionBlockEnd = new Vector3(worldCoordinateEnd);
            for (int i = sectionIndexStart; i <= sectionIndexEnd; i++)
            {
                ChunkSection section = chunk.Sections[i];

                sectionBlockStart.Y = Math.Max(worldCoordinateStart.Y - i * 16, 0);

                sectionBlockEnd.Y = Math.Min(sectionBlockStart.Y + (worldCoordinateEnd.Y - (i * 16 + sectionBlockStart.Y)), 16);
                section.SetBlocks(blocks, sectionBlockStart, sectionBlockEnd, chunkWorldCoordinate, airOverwrites);
            }
        }

        private Chunk CreateNewChunk(int chunkX, int chunkZ, BiomeType biomeType)
        {
            NBT.Tag level = new NBT.Tag();
            level.Name = "Level";
            level.Type = NBT.TagType.Compound;
            level.Payload = new NBT.CompoundTagPayload(new List<NBT.Tag>()
            {
                new NBT.Tag() { Name = "xPos", Type = NBT.TagType.Int, Payload = new NBT.IntPayload(chunkX) },
                new NBT.Tag() { Name = "zPos", Type = NBT.TagType.Int, Payload = new NBT.IntPayload(chunkZ) },
                new NBT.Tag() { Name = "LastUpdate", Type = NBT.TagType.Long, Payload = new NBT.LongPayload(0) },
                new NBT.Tag() { Name = "TerrainPopulated", Type = NBT.TagType.Byte, Payload = new NBT.BytePayload(0) },
                new NBT.Tag() { Name = "InhabitedTime", Type = NBT.TagType.Long, Payload = new NBT.LongPayload(0) },
                CreateBiomeTag(biomeType),
                CreateHeightMapTag(),
                CreateSections(),
                new NBT.Tag() { Name = "Entities", Type = NBT.TagType.List, Payload = new NBT.ListTagPayload(new List<NBT.TagPayload>(), NBT.TagType.Byte) },
                new NBT.Tag() { Name = "TileEntities", Type = NBT.TagType.List, Payload = new NBT.ListTagPayload(new List<NBT.TagPayload>(), NBT.TagType.Byte) }
            });

            NBT.Tag root = new NBT.Tag();
            root.Name = "";
            root.Type = NBT.TagType.Compound;
            root.Payload = new NBT.CompoundTagPayload(new List<NBT.Tag>() { level });
            Chunk chunk = new Chunk(root);

            chunks.Add(new Vector2(chunkX, chunkZ), chunk);

            return chunk;
        }

        private NBT.Tag CreateBiomeTag(BiomeType biomeType)
        {
            NBT.Tag biomeTag = new NBT.Tag();
            biomeTag.Name = "Biomes";
            biomeTag.Type = NBT.TagType.ByteArray;

            byte[] biomeData = new byte[256];
            for (int i = 0; i < 256; ++i)
                biomeData[i] = (byte)biomeType;
            biomeTag.Payload = new NBT.ByteArrayPayload(biomeData);
            return biomeTag;
        }

        private NBT.Tag CreateHeightMapTag()
        {
            NBT.Tag biomeTag = new NBT.Tag();
            biomeTag.Name = "HeightMap";
            biomeTag.Type = NBT.TagType.IntArray;

            int[] heightMap = new int[256];
            for (int i = 0; i < 256; ++i)
                heightMap[i] = 0;
            biomeTag.Payload = new NBT.IntArrayPayload(heightMap);
            return biomeTag;
        }

        private NBT.Tag CreateSections()
        {
            NBT.Tag sectionTag = new NBT.Tag();
            sectionTag.Type = NBT.TagType.List;
            sectionTag.Name = "Sections";
            sectionTag.Payload = new NBT.ListTagPayload(new List<NBT.TagPayload>(), NBT.TagType.Compound);
            return sectionTag;
        }
    }
}
